<?php

/**
 *  Base model class 
 */
abstract class fvRoot implements ArrayAccess {

    /**
     * fields array, each field has type `field_name` => array (value, type, changed). Not include id field and auto date fields
     */ 
    protected $fields;
    protected $key;
    protected $keyName;
    protected $tableName;
    protected $valid;
    
    const UPDATE = 'Update';
    const INSERT = 'Insert';
    protected static $databaseOperation = array(self::UPDATE => DB_AUTOQUERY_UPDATE, self::INSERT => DB_AUTOQUERY_INSERT);
    protected static $serializeTypes = array('array', 'object');
    
/**
     * 
     */
    function __construct ($fields, $tableName, $keyName = "id") {
        $this->fields = $fields;
        $this->tableName = $tableName;
        $this->keyName = $keyName;  
        $this->valid = array();
    }
    
    protected function _setUnChanged($fieds = null) {
        if (is_null($fieds)) {
            foreach ($this->fields as &$field) {
                $field['changed'] = false;
            }
        }
        else {
            if (!is_array($fieds)) $fieds = array($fieds);
            
            foreach ($fieds as $field) {
                if (isset($this->fields[$field]))
                    $this->fields[$field]["changed"] = false;
                else throw new EFieldError("No field'{$field}' is specified");
            }
        }
    }
    
    public static function getSerializedTypes() {
        return self::$serializeTypes;
    }
    
    function getFieldList() {
        return array_keys($this->fields);
    }

    function isChanged($fieldName) {
        if (isset($this->fields[$fieldName]))
            return $this->fields[$fieldName]["changed"];
        else throw new EFieldError("No field '{$fieldName}' is specified");
    }
    
    function get($fieldName, $defaultValue = '') {
        if (isset($this->fields[$fieldName])) {
            $return = (!is_null($this->fields[$fieldName]["value"]))?$this->fields[$fieldName]["value"]:$defaultValue;
            if (gettype($return) != $this->fields[$fieldName]["type"] && $this->fields[$fieldName]["type"] != 'date') {
                @settype($return, $this->fields[$fieldName]["type"]);
            }
            
            return $return;
        }
        else throw new EFieldError("No field'{$fieldName}' is specified");
        
    }
    
    function set($fieldName, $newValue) {
        if (isset($this->fields[$fieldName])) { 
            if (($this->fields[$fieldName]["value"] != $newValue) || ($this->isNew())) {
                $valid = true;
                
                if (isset($this->fields[$fieldName]["validate"]) && $this->fields[$fieldName]["validate"]) {
                    $validateMethod = "validate" . ucfirst(strtolower($fieldName));

                    if (method_exists($this, $validateMethod)) {
                        $valid = $this->$validateMethod($newValue);
                    }
                }

                if ($valid) {
                    @settype($newValue, $this->fields[$fieldName]['type']);
                    
                    if ($this->fields[$fieldName]['pre_func'] && function_exists($this->fields[$fieldName]['pre_func'])) { 
                        $newValue = call_user_func($this->fields[$fieldName]['pre_func'], $newValue);
                    }

                    $this->fields[$fieldName]["value"] = $newValue;
                    $this->fields[$fieldName]["changed"] = true;
                }
                else {
                    $this->fields[$fieldName]["value"] = $newValue;
                    
                    return false;
                }
            }
            else return false;
                    
        }
        else throw new EFieldError("No field '{$fieldName}' is specified");
        
        return true;
    }
    
    function addField ($fieldName, $type, $value = null) {
        if (empty($fieldName) || empty($type)) throw new EFieldError("field name and type are required");
        
        $this->fields[$fieldName] = array ("value" => $value, "type" => $type, "changed" => false);
    }
    
    function removeField ($fieldName) {
        if (empty($fieldName)) throw new EFieldError("field name is required");
        if (!isset($this->fields[$fieldName])) throw new EFieldError("No field'{$fieldName}' is specified");
        
        unset($this->fields[$fieldName]);
    }
    
    function __get($name) {
        return $this->get($name, null);
    }
    
    function __set($name, $value) {
        return $this->set($name, $value);
    }
    
    function save($logging = true) {
        if ($this->isNew()) {
            return $this->saveToDatabase(self::INSERT, $logging);
        } else {
            return $this->saveToDatabase(self::UPDATE, $logging);
        }
        return false;
    }
    
    protected function saveToDatabase($saveType, $logging) {
        if ($this->isValid()) {
            $insertList = array();
            
            foreach ($this->getFieldList() as $field) {
                if ($this->isChanged($field) || $this->autoValue($field, $saveType)) {
                    if (in_array($this->getFieldType($field), self::$serializeTypes)) {
                         $insertList[$field] = serialize($this->get($field));
                    }
                    else {
                        if ($this->getFieldType($field) == 'set') {
                            $insertList[$field] = implode(',',$this->get($field));
                        } else $insertList[$field] = $this->get($field);
                    }
                }
            }
            
            if (count($insertList) > 0) {
                if ($saveType == self::INSERT) {
                    $dbResult = fvSite::$DB->autoExecute($this->getTableName(), $insertList, self::$databaseOperation[$saveType]);
                } else {
                    
                    if (is_array($keyNames = $this->getPkName())) {
                        $where = '';
                         foreach ($keyNames as $key) {
                             $where .= (($where)?" AND ":'') . $key . " = " . $this->getPk($key);
                         }
                    } else {
                        $where = $this->getPkName() . " = " . $this->getPk();
                    }
                    
                    $dbResult = fvSite::$DB->autoExecute($this->getTableName(), $insertList, self::$databaseOperation[$saveType], $where);
                }
                if (DB::isError($dbResult)) {                    
                    throw new EDatabaseError($dbResult->getMessage());
                }
            }
            
            if ($saveType == self::INSERT) {
                $this->setPk(fvSite::$DB->getLastId());
            }
            $this->_setUnChanged();
            
            if ($logging && $this instanceof iLogger) {
                $this->putToLog(($saveType == self::INSERT)?Log::OPERATION_INSERT:Log::OPERATION_UPDATE);
            }

            return true;
        } else {
            if ($logging && $this instanceof iLogger) {
                $this->putToLog(Log::OPERATION_ERROR);
            }
            
            return false;
        }
    }
    
    function delete() {
        if ($this->isNew()) return false;
        
        $primaryKey = $this->getPkName();
        $tableName = $this->getTableName();
        
        $phc = fvSite::$DB->Prepare("DELETE FROM $tableName WHERE $primaryKey = ?");
        
        if (DB::isError(fvSite::$DB->Execute($phc, array($this->getPk())))) 
            throw new EDatabaseError("Can't delete record from database.");
        
        if ($this instanceof iLogger) {
            $this->putToLog(Log::OPERATION_DELETE);
        }
            
        return true;
    }
    
    /**
     * Fill Object by array
     *
     */
    function hydrate($MAP) {
        if (!is_array($MAP)) throw new EModelError("Can't create object from non array");
        
        foreach ($MAP as $field => $value) {
            if (isset($this->fields[$field])) {
                
                if (in_array($this->fields[$field]['type'], self::$serializeTypes) && (gettype($value) != $this->fields[$field]['type'])) {
                    $value = unserialize($value);
                }
                
                if ($this->fields[$field]['type'] == 'set' && (gettype($value) != $this->fields[$field]['type'])) {
                    $value = explode(',', $value);
                }
                                
                @settype($value, $this->fields[$field]['type']);
                
                $this->fields[$field]["value"] = $value;
                $this->fields[$field]["changed"] = false;
            }
        }
        
        
        if (is_array($this->keyName)) {
            foreach ($this->keyName as $keys) {
                if (isset($MAP[$keys])) 
                $this->key[$keys] = $MAP[$keys];
            }
        } else {
            if (isset($MAP[$this->keyName])) 
                $this->key = $MAP[$this->keyName];
        }
    
        
        $this->valid = array();
    }
    
    function toHash() {
        $result = array();
        foreach ($this->fields as $name => $values) {
            $result[$name] = $values['value'];
        }
        
        if (is_array($this->keyName)) {
            foreach ($this->keyName as $keys) {
                $result[$keys] = $this->key[$keys];
            }
        } else {
             $result[$this->keyName] = $this->key;
        }
        
        return $result;
    }
    
    public function updateFromRequest($REQUEST) {
        
        foreach ($this->fields as $fieldName => $field) {
            switch ($field['type']) {
                case 'array':
                case 'set':
                    $this->fields[$fieldName]['value'] = array();
                    $this->fields[$fieldName]['changed'] = true;
                    break;
                case 'bool':
                    $this->fields[$fieldName]['value'] = false;
                    $this->fields[$fieldName]['changed'] = true;
                    break;
            }
        }
        
        foreach ($REQUEST as $field => $value) {
            if (isset($this->fields[$field])) {
                $this->set($field, $value);
            }
        }
        return $this->isValid();
    }
    
    function getPk($keyName = null) {
        if (is_null($keyName))
            return $this->key;
        else return $this->key[$keyName];
    }
    
    function setPk($key, $keyName = null) {
        if (is_null($keyName) && !is_array($this->key))
            $this->key = $key;
        else {
            $this->key[$keyName] = $key;
        }
    }
    
    function getPkName() {
        return $this->keyName;
    }

    protected function setPkName($keyName) {
        $this->keyName = $keyName;
    }
    
    function getTableName () {
        return $this->tableName;
    }
    
    protected function setTableName ($tableName) {
        $this->tableName = $tableName;
    }
    
    function isValid() {
        return (count($this->valid) == 0);
    }
    
    function getValidationResult () {
        return $this->valid;
    }
    
    protected function setValidationResult ($field, $valid, $message = "") {
        if (!$valid) $this->valid[$field] = fvSite::$fvConfig->get("entities.{$this->currentEntity}.fields.{$field}.invalid_string", $message);
        else if (isset($this->valid[$field])) unset($this->valid[$field]);
    }
    
    function autoValue ($filedName, $action) {
        if (isset($this->fields[$filedName]['auto_set']) && (strtolower($this->fields[$filedName]['auto_set']) == strtolower($action) || strtolower($this->fields[$filedName]['auto_set']) == "any")) {

            switch ($this->fields[$filedName]['type']) {
                case "integer":
                    if ($this->fields[$filedName]['auto_value'] == "inc()") $this->fields[$filedName]["value"]++;   
                    else if ($this->fields[$filedName]['auto_value'] == "dec()") $this->fields[$filedName]["value"]--;
                    else if ($this->fields[$filedName]['auto_value'] == "now()") $this->fields[$filedName]["value"] = time();
                    else $this->fields[$filedName]["value"] = intval($this->fields[$filedName]['auto_value']);
                    break;
                case "date":
                    if ($this->fields[$filedName]['auto_value'] == "now()") $this->fields[$filedName]["value"] = date("Y-m-d H:i:s");
                    else $this->fields[$filedName]["value"] = $this->fields[$filedName]['auto_value'];
                    break;
                case "bool":
                    $this->fields[$filedName]["value"] = (bool)$this->fields[$filedName]['auto_value'];
                    break;
                case "string":
                    $this->fields[$filedName]["value"] = (string)$this->fields[$filedName]['auto_value'];
                    break;
            }

            return true;
        }
        
        return false;
    }

    function isNew() {
        return empty($this->key);
    }

    function hasField($fieldName) {
        return isset($this->fields[$fieldName]);
    }
    
    function getFieldType($fieldName) {
        return $this->fields[$fieldName]['type'];
    }
    
    function offsetExists($fieldName) {
        return $this->hasField($fieldName);
    }

    function offsetGet($fieldName) {
        return $this->get($fieldName, null);
    }

    function offsetUnset($fieldName) {
        return $this->removeField($fieldName);
    }
    
    function offsetSet($fieldName, $newValue) {
        if ($this->hasField($fieldName))
            return $this->set($fieldName, $newValue);
        else $this->addField($fieldName, ($newValue)?gettype($newValue):'string', $newValue);
    }
}

?>
